Chomu's Blog.

>

Posts

GitHub

홈서버로 CDN 만들기

목차

개요

이번에 OSSCA 의 fedify 프로젝트에 참여하게 됐는데, 해당 라이브러리를 사용하는 액티비티 펍 인스턴스를 만들려다 보니 CDN이 필요했다. 처음에는 그냥 파일로 저장할까 했다가 이참에 남는 컴퓨터로 공부할 겸 CDN을 만들어보자고 생각했다.

NGINX

단순 파일 미러를 위해 NGINX를 사용했다. 우분투에 설치했기 때문에 apt 명령어로 설치했다.

sudo apt install nginx
sudo systemctl enable nginx
sudo systemctl start nginx

캐시용 디렉터리 /var/cache/nginx 를 만들고 권한을 설정한다.

sudo mkdir -p /var/cache/nginx
sudo chown www-data:www-data /var/cache/nginx

캐시용 NGINX 설정 파일을 /etc/nginx/sites-available/cdn.conf 에 작성했다.

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=cdn_cache:10m inactive=60m max_size=5g;
 
server {
    listen <포트 번호>;
    server_name <서버 도메인>;
 
    # /media/* 요청 처리
    location /media/ {
        alias <미디어 파일 경로>;
        try_files $uri =404;
 
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header X-CDN-Status "Static Alias";
    }
}

<서버 도메인> 를 거쳐 <포트 번호>로 들어오는 /media/* 요청은 <미디어 파일 경로>에 있는 파일을 반환하고, try_files $uri =404; 는 요청한 파일이 없으면 404 에러를 반환한다. add_header 지시어는 응답 헤더에 캐시 관련 정보를 추가한다. Cache-Control 헤더는 브라우저 캐시를 설정하고, X-CDN-Status 헤더는 CDN 상태를 나타낸다.

홈서버에 도메인 설정은 Cloudflare로 로컬서버 호스팅하기 참고.

설정 파일을 활성화하려면 /etc/nginx/sites-enabled/ 디렉터리에 심볼릭 링크를 생성한다.

sudo ln -s /etc/nginx/sites-available/cdn.conf /etc/nginx/sites-enabled/

NGINX 설정을 테스트하고, 문제가 없으면 재시작한다. 방화벽도 설정해주자.

sudo nginx -t
sudo systemctl restart nginx
sudo ufw allow 'Nginx Full'

만약 주소로 접속은 했는데 NGINX 에러가 발생한다면 sudo tail -f /var/log/nginx/error.log 명령어로 에러 로그를 확인해보자. 나는 stat() "<파일 경로>" failed (13: Permission denied) 라는 에러가 났었다. 알고 보니 <미디어 파일 경로> 경로 뿐만이 아니라 각 부모 경로, 예를 들어 /home/user/media/ 라면 /home, /home/user, /home/user/media 모두 NGINX가 읽을 수 있어야 했던 것이다. 만약 나와 비슷한 오류가 났다면 각 경로 별로 권한을 확인하고, NGINX가 읽을 수 있도록 권한을 설정해주자.

sudo chmod o+x /home
sudo chmod o+x /home/ubuntu
...
sudo chmod o+x /home/ubuntu/img

업로드 구현하기

CDN에 파일을 업로드하는 기능은 NGINX로는 구현할 수 없으므로, Deno를 사용해 간단한 웹 서버를 만들었다. 잘 올라가는 지 확인만 하는 용도이므로, 그냥 LLM 을 사용해 간단한 파일 업로드 서버를 만들었다.

import { unescape } from "@std/html/entities";
 
const SERVER_DOMAIN = Deno.env.get("SERVER_DOMAIN")!;
const SAVE_PATH = Deno.env.get("SAVE_PATH")!;
if (!SERVER_DOMAIN || !SAVE_PATH) {
  throw new Error("SERVER_DOMAIN and SAVE_PATH environment variables must be set.");
}
if (!SAVE_PATH.endsWith("/")) {
  throw new Error("SAVE_PATH must end with a '/'");
}
 
async function rootHandler(req: Request) {
  const method: string = req.method;
 
  if (method === "POST") {
    const formData: FormData = await req.formData();
    const file: File | null = formData?.get("file") as File;
 
    if (!file) {
      return new Response("File required but not provided.", { status: 400 });
    }
    const ext = file.name.split(".").pop()?.toLowerCase();
 
    const fileName: string = crypto.randomUUID().replaceAll("-", "") + "." +
      ext;
 
    const filePath = `${SAVE_PATH}${fileName}`;
 
    await Deno.mkdir(SAVE_PATH, { recursive: true });
    await Deno.writeFile(filePath, new Uint8Array(await file.arrayBuffer()));
 
    // 업로드된 파일로 리다이렉트
    return Response.redirect(`${SERVER_DOMAIN}/media/data/${fileName}`, 303);
  }
 
  const inputForm = `
    &#x3C;form method=&#x22;POST&#x22; enctype=&#x22;multipart/form-data&#x22;&#x3E;
      &#x3C;input name=&#x22;file&#x22; type=&#x22;file&#x22; /&#x3E;
      &#x3C;button type=&#x22;submit&#x22;&#x3E;Upload&#x3C;/button&#x3E;
    &#x3C;/form&#x3E;
  `;
  return new Response(
    unescape(inputForm),
    {
      headers: {
        "Content-Type": "text/html; charset=utf-8",
      },
    },
  );
}
 
Deno.serve({port:9000},rootHandler);

~~생각보다 더 대충인듯 inputForm 봐 개끔직~~ 파일 올라가는 것만 확인하면 바로 CDN 서버로 리다이렉트한다. 다만 Deno 는 보안을 위해 외부 파일 시스템에 대한 접근을 제한하는 듯했다. 그래서 미디어 파일 저장 경로에 업로드 경로 심볼릭 링크로 연결해주었다.

sudo ln -s <미디어 파일> <업로드>

Tailscale 을 사용해 개인 도메인으로 연결해서 인증은 구현하지 않았다.

확인

잘 되는 듯 하다.